Day20 結尾留了兩個還沒定案的東西:Graphify 期待的節點欄位、邊欄位具體要叫什麼名字,以及這些資料具體要怎麼從
Index轉換出來。今天要把這兩個「還沒定案」都定案,讓brain-cli真正產出一份 Graphify 能吃的 JSON 檔案。
Day20 只定了兩個道理:節點需要穩定唯一的識別碼加可讀的顯示名稱,邊需要來源與目標節點的識別碼,資料來源就是既有的 Index。今天要做的事情很單純——把這兩個道理,變成 internal/vault 裡具體的 Go 結構、一個新的 brain graph 指令,以及一份實際印得出來的 JSON。
Frontmatter.ID,不選 TitleDay20 講的「穩定且唯一」不是隨口一句話,落地時第一個要決定的就是:這個識別碼具體對應筆記的哪個既有欄位?
候選有兩個:Frontmatter.Title 跟 Frontmatter.ID。Title 看起來更直覺,但 Index.indexTitleAndAliases 早就證明了它不保證唯一——Conflicts 這個機制存在的理由,就是因為兩篇筆記的標題(或別名)真的可能撞名。如果拿 Title 當節點識別碼,一旦撞名,Graphify 會把兩個不同節點的邊全部算到同一個節點 ID 上,畫出一張錯的圖。FilePath 也考慮過,但檔案路徑會隨搬移、重新命名而改變,不符合「穩定」的要求。
最後選的是 Frontmatter.ID——capture 在建立筆記時就以 YYYYMMDD-HHMMSS 格式自動產生,是筆記一出生就固定、不隨檔案系統操作變動的欄位。所以 GraphNode.ID 對應 Frontmatter.ID,GraphNode.Label 對應 Frontmatter.Title——顯示名稱可以撞名沒關係,反正它只負責給人看,不負責讓工具辨識節點是不是同一個。
BuildGraph 產生節點清單的方式,是直接走訪 Scan 回傳的整份 notes 切片,不是走訪「有邊的筆記」。這代表一篇完全沒有 outbound、也沒有 inbound 連結的孤立筆記,一樣會出現在 Graph.Nodes 裡,只是不會出現在任何一條邊的 Source/Target 中。
這個決定跟 brain health 的 OrphanNotes 概念是一致的:孤立筆記本身是 vault 裡一個有意義的狀態,知識圖譜要完整呈現整座 vault 的真實樣子。如果只列出「至少有一條邊」的筆記,Graphify 畫出來的圖會讓人誤以為孤立筆記根本不存在於這座 vault,這跟「匯出資料」這個目標本身是矛盾的。
brain health 是同一份資料的兩種呈現角度Index.Unresolved 已經記錄了所有解析失敗的連結,brain health 早就用它產出 BrokenLinks。BuildGraph 對每篇筆記的 idx.Outbound[note] 逐一呼叫 idx.Resolve,解析成功才追加一條邊,解析失敗直接跳過——不另外累積一份「圖匯出專用」的斷鏈清單。
理由很直接:邊的語意本來就要求兩端都指向真實存在的節點,一條指向不存在節點的邊,Graphify 沒辦法正確畫在圖上,這正是 Day20 定的「邊需要指向存在節點」道理的直接應用。而如果讓 BuildGraph 自己再維護一份斷鏈記錄,Unresolved 跟這份新記錄就變成兩套各自更新的資料,日後很容易兩邊不同步。所以斷鏈該去哪裡查,答案沒有變——還是 brain health,brain graph 只是從另一個角度(哪些連結變成了邊)呈現同一份 Unresolved 資料,不重複發明一套新的斷鏈偵測邏輯。
brain graph --json 的實際輸出新增的 graph 子指令延續 Day18 定下的 --json 慣例:不帶旗標時印純文字(節點清單、邊清單、結尾統計),帶上 --json 時印一個單一 JSON 物件到 stdout。
對現有 demo vault 執行 brain graph,純文字模式印出 14 個節點、14 條邊:
- [20260815-093000] Cobra CLI 框架
- [20260815-090000] PARA 筆記法
...
20260815-093000 -> 20260819-220325
20260815-093000 -> 20260819-220757
20260815-093000 -> 20260819-223000
...
共 14 個節點,14 條邊
brain graph --json 對同一個 vault 執行,輸出(實際不換行、不縮排,這裡重新排版方便閱讀):
{
"nodes": [
{"id": "20260815-093000", "label": "Cobra CLI 框架"},
{"id": "20260815-090000", "label": "PARA 筆記法"},
...
],
"edges": [
{"source": "20260815-093000", "target": "20260819-220325"},
{"source": "20260815-093000", "target": "20260819-220757"},
...
],
"nodeCount": 14,
"edgeCount": 14
}
nodeCount/edgeCount 跟純文字模式結尾那句統計對得上。實際核對 demo vault:8 篇孤立筆記(PARA 筆記法、Obsidian Wikilink 語法備忘 等)全部出現在 nodes 裡,但沒有任何一個的 id 出現在 edges 的 source/target 中——跟決策裡「孤立筆記仍列為節點」的道理完全對上。這個 vault 目前沒有斷鏈,brain health 回報「0 筆斷鏈」,graph 的邊數也剛好等於所有可解析連結的總數,兩邊資料一致。
今天做的事情,範圍刻意收得很窄:只把 Index 轉換成 Graphify 能吃的節點/邊資料,寫成一個唯讀、不動任何檔案的 brain graph 指令。沒有做的事情也同樣刻意——沒有在這張圖上找核心樞紐、找斷點,那些分析邏輯今天完全沒碰。
👉 明天 Day22,要在今天匯出的這張圖上,動手找出 vault 裡連結最密集的核心樞紐筆記,以及連結最脆弱、一斷就可能讓整片知識孤立的斷點。今天的 brain graph 匯出,就是 Day22 分析邏輯站上去的地基。我們明天見!